package org.rcgwt.client.ui.calendar.controller;

import java.util.Date;
import java.util.Map;

import org.rcgwt.client.ui.calendar.RichCalendar;
import org.rcgwt.client.ui.calendar.event.HasInvalidDateHandlers;
import org.rcgwt.client.ui.calendar.event.HasModelChangedHandlers;
import org.rcgwt.client.ui.calendar.event.HasSelectionChangedHandlers;
import org.rcgwt.client.ui.calendar.event.InvalidDateEvent;
import org.rcgwt.client.ui.calendar.event.InvalidDateHandler;
import org.rcgwt.client.ui.calendar.event.ModelChangedEvent;
import org.rcgwt.client.ui.calendar.event.ModelChangedHandler;
import org.rcgwt.client.ui.calendar.event.SelectionChangedEvent;
import org.rcgwt.client.ui.calendar.event.SelectionChangedHandler;
import org.rcgwt.client.ui.calendar.model.impl.CalendarModel;
import org.rcgwt.client.ui.calendar.model.impl.Event;
import org.rcgwt.client.ui.calendar.model.impl.EventType;
import org.rcgwt.client.ui.calendar.model.impl.SelectionCardinality;
import org.rcgwt.client.ui.calendar.model.impl.SelectionType;
import org.rcgwt.client.ui.calendar.renderer.IRenderer;
import org.rcgwt.client.ui.themes.ITheme;
import org.rcgwt.client.ui.themes.StandardTheme.RendererType;

import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.Widget;

/**
 * A MVC calendar component : the controller that dispatches action from
 * renderer to model and action from model to renderer
 * 
 * @author Elian ORIOU
 * 
 */

@SuppressWarnings("deprecation")
public class CalendarController extends Widget implements
		HasModelChangedHandlers, HasSelectionChangedHandlers,
		HasInvalidDateHandlers, FocusHandler, KeyDownHandler {

	private final RichCalendar component;
	private final CalendarModel model;

	public CalendarController(RichCalendar component, ITheme theme,
			CalendarModel model, Map<RendererType, IRenderer> renderers) {
		this.component = component;
		this.model = model;
	}

	/**
	 * Handle method
	 * 
	 * @param mask
	 */

	public void handle(CalendarAction mask) {
		/* Actions sent by a renderer to be dispatched to another */

		int displayedYear = model.getDisplayedYear();
		int displayedMonth = model.getDisplayedMonth();

		switch (mask) {
		case INIT:
			/* Initializing the Grid with the current month & the current year */
			fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH
					| CalendarChangesMask.SPECIFIED_YEAR, model
					.peekDisplayedDate());
			break;
		case REFRESH_GRID:
			/*
			 * To refresh the grid (when an action causes a model changes for
			 * instance)
			 */
			fireModelChangedEvent(CalendarChangesMask.REFRESH_GRID, model
					.peekDisplayedDate());
			break;
		case REFRESH_CALENDAR:
			/* To refresh the all CALENDAR */
			fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH, model
					.peekDisplayedDate());
			fireModelChangedEvent(CalendarChangesMask.SPECIFIED_YEAR, model
					.peekDisplayedDate());
			fireModelChangedEvent(CalendarChangesMask.SELECTION_TYPE, model
					.peekDisplayedDate());
			fireModelChangedEvent(CalendarChangesMask.REFRESH_GRID, model
					.peekDisplayedDate());
			break;
		case PREVIOUS_YEAR:
			/* Min & Max dates management */
			if (isDisplayableMin(displayedMonth, displayedYear - 1, 0) == false
					|| isDisplayableMax(displayedMonth, displayedYear - 1, 0) == false) {
				fireModelChangedEvent(CalendarChangesMask.MIN_OVERFLOW, null);
				return;
			}

			/* To change displayed year */
			if (displayedYear > 0) {
				model.decreaseDisplayedYear();
			}
			/* Launch Event to renderer */
			fireModelChangedEvent(CalendarChangesMask.SPECIFIED_YEAR, model
					.peekDisplayedDate());
			break;
		case NEXT_YEAR:
			if (isDisplayableMin(displayedMonth, displayedYear + 1, 0) == false
					|| isDisplayableMax(displayedMonth, displayedYear + 1, 0) == false) {
				fireModelChangedEvent(CalendarChangesMask.MAX_OVERFLOW, null);
				return;
			}
			/* To change displayed year */
			model.increaseDisplayedYear();
			/* Launch Event to renderer */
			fireModelChangedEvent(CalendarChangesMask.SPECIFIED_YEAR, model
					.peekDisplayedDate());
			break;
		case PREVIOUS_MONTH:
			/* To change displayed month */
			if (displayedMonth == 0) {
				if (setDisplayedMonth(11) == false) {
					fireModelChangedEvent(CalendarChangesMask.MIN_OVERFLOW,
							null);
					return;
				}
				model.decreaseDisplayedYear();
				/* Launch Event to renderer */
				fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH
						| CalendarChangesMask.SPECIFIED_YEAR, model
						.peekDisplayedDate());
			} else {
				if (setDisplayedMonth(model.getDisplayedMonth() - 1) == false) {
					fireModelChangedEvent(CalendarChangesMask.MIN_OVERFLOW,
							null);
					return;
				}
				/* Launch Event to renderer */
				fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH,
						model.peekDisplayedDate());
			}
			break;
		case NEXT_MONTH:
			/* To change displayed month */
			if (displayedMonth == 11) {
				if (setDisplayedMonth(0) == false) {
					fireModelChangedEvent(CalendarChangesMask.MAX_OVERFLOW,
							null);
					return;
				}
				model.increaseDisplayedYear();
				/* Launch Event to renderer */
				fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH
						| CalendarChangesMask.SPECIFIED_YEAR, model
						.peekDisplayedDate());
			} else {
				if (setDisplayedMonth(model.getDisplayedMonth() + 1) == false) {
					fireModelChangedEvent(CalendarChangesMask.MAX_OVERFLOW,
							null);
					return;
				}
				/* Launch Event to renderer */
				fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH,
						model.peekDisplayedDate());
			}
			break;
		case SELECT_YEAR_CURSOR:
			/*
			 * When the user selects the year label (in order to navigate with
			 * keyboard for instance)
			 */
			fireModelChangedEvent(CalendarChangesMask.CLEAR_SELECTION, null);
			model.setSelectionType(SelectionType.YEAR);
			fireModelChangedEvent(CalendarChangesMask.SELECT_YEAR, null);
			break;
		case SELECT_MONTH_CURSOR:
			/*
			 * When the user selects the month label (in order to navigate with
			 * keyboard for instance)
			 */
			fireModelChangedEvent(CalendarChangesMask.CLEAR_SELECTION, null);
			model.setSelectionType(SelectionType.MONTH);
			fireModelChangedEvent(CalendarChangesMask.SELECT_MONTH, null);
			break;
		case SELECT_WEEK_CURSOR:
			/* To select a week label */
			if (model.getSelectionType() == SelectionType.WEEK) {
				fireModelChangedEvent(CalendarChangesMask.SELECT_WEEK, null);
			}
			break;
		case UNSELECT_WEEK:
			/* To deselect a week label */
			fireModelChangedEvent(CalendarChangesMask.UNSELECT_WEEK, null);
			break;
		case SELECT_WEEK_RANGE:
			/* To select a week range (limited by 2 values) */
			if (model.getSelectionType() == SelectionType.WEEK) {
				fireModelChangedEvent(CalendarChangesMask.SELECT_WEEK_RANGE,
						null);
			}
			break;
		case UNSELECT_WEEK_RANGE:
			/* To deselect a week range (limited by 2 values) */
			fireModelChangedEvent(CalendarChangesMask.UNSELECT_WEEK_RANGE, null);
			break;
		case PREVIOUS_CONTEXT:
			/* To changes selection context (YEAR | MONTH | WEEK | DAYS | DATE) */
			fireModelChangedEvent(CalendarChangesMask.PREVIOUS_CONTEXT, null);
			break;
		case NEXT_CONTEXT:
			/* To changes selection context */
			fireModelChangedEvent(CalendarChangesMask.NEXT_CONTEXT, null);
			break;
		case DISPLAY_TODAY:
			int currentDate = model.getCurrentDate().getDate();
			int currentYear = model.getCurrentYear();
			int currentMonth = model.getCurrentMonth();
			/* To display the current month */
			if (isDisplayableMin(currentMonth, currentYear, 0) == false) {
				return;
			}
			displayMonth(currentMonth, currentYear);
			model.setCursorPosition(currentDate);
			setSelectedDate(model.getCurrentDate());
			fireModelChangedEvent(CalendarChangesMask.SPECIFIED_YEAR
					| CalendarChangesMask.SPECIFIED_MONTH, model
					.getCurrentDate());
			break;
		}
	}

	/**
	 * Manages focus behavior
	 */

	@Override
	public void onFocus(FocusEvent event) {
		if (model.getSelectionType() != SelectionType.NONE) {
			model.setSelectionType(SelectionType.NONE);
		}
	}

	/**
	 * Returns the calendar model
	 * 
	 * @return the model
	 */

	public CalendarModel getModel() {
		return model;
	}

	/* Component Methods */

	/**
	 * Sets the calendar value to <i>d</i>
	 * 
	 * @param d
	 *            The date
	 */

	public boolean setValue(Date d) {
		if (model.getManager().isSelectable(d) == false) {
			return false;
		}
		if (isDisplayableMin(d.getMonth(), d.getYear() + 1900, d.getDate()) == false
				|| isDisplayableMax(d.getMonth(), d.getYear() + 1900, d
						.getDate()) == false) {
			return false;
		}
		boolean dy = setDisplayedYear(d.getYear() + 1900);
		boolean dm = setDisplayedMonth(d.getMonth());
		model.setSelectedDate(d);
		displayMonth(model.getDisplayedMonth(), model.getDisplayedYear());
		return dy && dm;
	}

	/**
	 * Displays a month
	 * 
	 * @param month
	 */

	public void displayMonth(int month) {
		model.setDisplayedMonth(month);
		Date d = model.peekDisplayedDate();
		d.setMonth(month);
		fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH, d);
	}

	/**
	 * Creates an event
	 */

	public void createEvent(final Event e) {
		model.getManager().addEvent(e);
		fireModelChangedEvent(CalendarChangesMask.APPLY_STYLES, model
				.getCurrentDate());
	}

	/**
	 * Sets the selected date to <i><s</i>
	 * 
	 * @param s
	 *            The selected date
	 */

	public void setSelectedDate(Date s) {
		if (model.getSelectionCardinality() == SelectionCardinality.NONE) {
			return;
		}
		model.setSelectedDate(s);
		launchValueChangeEvent(s);
	}

	/* Model Interaction method */

	public void setCurrentFirstDay(int currentFirstDay) {
		model.setCurrentFirstDay(currentFirstDay);
	}

	public void setSelection(int selection) {
		model.setCursorPosition(selection);
	}

	public void increaseSelection() {
		model.increaseSelection();
	}

	public void decreaseSelection() {
		model.decreaseSelection();
	}

	public void setSelectedWeekLine(int selectedWeek) {
		model.setSelectedWeek(selectedWeek);
	}

	public void increaseSelectedWeek() {
		model.increaseSelectedWeek();
	}

	public void decreaseSelectedWeek() {
		model.decreaseSelectedWeek();
	}

	public void setSelectedStartWeek(int startWeek) {
		model.setSelectedStartWeek(startWeek);
	}

	public void setSelectedEndWeek(int endWeek) {
		model.setSelectedEndWeek(endWeek);
	}

	public void setSelectionType(SelectionType selectionContext) {
		model.setSelectionType(selectionContext);
		fireModelChangedEvent(CalendarChangesMask.SELECTION_TYPE, null);
	}

	public void removeDeselectedDate(Date d) {
		model.removeDeselectedDate(d);
	}

	public void setSelectedColumn(int i) {
		model.setSelectedColumn(i);
	}

	public boolean setDisplayedMonth(int month) {
		int year = model.getDisplayedYear();

		Date min = model.getMinDate();
		Date max = model.getMaxDate();

		if (min != null) {
			int minYear = min.getYear() + 1900;
			int minMonth = min.getMonth();
			if (minYear == year && month == minMonth) {
				model.setDisplayedMonth(month);
				return true;
			}
		}
		if (max != null) {
			int maxYear = max.getYear() + 1900;
			int maxMonth = max.getMonth();
			if (maxYear == year && month == maxMonth) {
				model.setDisplayedMonth(month);
				return true;
			}
		}
		if (isDisplayableMax(month, year, 0) == true
				&& isDisplayableMin(month, year, 0) == true) {
			model.setDisplayedMonth(month);
			return true;
		}
		return false;
	}

	public boolean setDisplayedYear(int year) {
		int month = model.getDisplayedMonth();
		int dyear = model.getDisplayedYear();
		if (year == dyear) {
			return true;
		}
		if (isDisplayableMin(month, year, 0) == true
				&& isDisplayableMax(month, year, 0) == true) {
			model.setDisplayedYear(year);
			return true;
		}
		return false;
	}

	public boolean isDisplayableMin(int month, int year, int date) {
		Date min = model.getMinDate();
		if (min == null) {
			return true;
		}
		if (year == min.getYear() + 1900) {
			if (month < min.getMonth()) {
				return false;
			}
			if (month == min.getMonth()) {
				if (date >= min.getDate()) {
					return true;
				}
				return false;
			}
			return true;
		} else if (year < min.getYear() + 1900) {
			return false;
		}
		return true;
	}

	public boolean isDisplayableMax(int month, int year, int date) {
		Date max = model.getMaxDate();
		if (max == null) {
			return true;
		}
		if (year == max.getYear() + 1900) {
			if (month > max.getMonth()) {
				return false;
			}
			if (month == max.getMonth()) {
				if (date <= max.getDate()) {
					return true;
				}
				return false;
			}
			return true;
		} else if (year > max.getYear() + 1900) {
			return false;
		}
		return true;
	}

	/**
	 * Changes the selection context (normally by a TAB_KEY press)
	 */

	public void changeContextCursor() {
		SelectionType type = model.getSelectionType();
		SelectionCardinality card = model.getSelectionCardinality();
		fireModelChangedEvent(CalendarChangesMask.CLEAR_SELECTION, null);

		if (card == SelectionCardinality.NONE) {
			if (type == SelectionType.NONE) {
				model.setSelectionType(SelectionType.YEAR);
				fireModelChangedEvent(CalendarChangesMask.SELECT_YEAR, null);
			} else if (type == SelectionType.YEAR) {
				model.setSelectionType(SelectionType.MONTH);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_YEAR, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_MONTH, null);
			} else if (type == SelectionType.MONTH) {
				model.setSelectionType(SelectionType.NONE);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_MONTH, null);
			}
		} else if (card == SelectionCardinality.SINGLE) {
			if (type == SelectionType.NONE) {
				model.setSelectionType(SelectionType.YEAR);
				fireModelChangedEvent(CalendarChangesMask.SELECT_YEAR, null);
			} else if (type == SelectionType.YEAR) {
				model.setSelectionType(SelectionType.MONTH);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_YEAR, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_MONTH, null);
			} else if (type == SelectionType.MONTH) {
				model.setSelectionType(SelectionType.DATE);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_MONTH, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_FIRST_DAY,
						null);
			} else if (type == SelectionType.DATE) {
				model.setSelectionType(SelectionType.NONE);
			}
		} else if (card == SelectionCardinality.MULTIPLE) {
			if (type == SelectionType.NONE) {
				model.setSelectionType(SelectionType.YEAR);
				fireModelChangedEvent(CalendarChangesMask.SELECT_YEAR, null);
			} else if (type == SelectionType.YEAR) {
				model.setSelectionType(SelectionType.MONTH);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_YEAR, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_MONTH, null);
			} else if (type == SelectionType.MONTH) {
				model.setSelectionType(SelectionType.DAYS);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_MONTH, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_FIRST_DAYS,
						null);
			} else if (type == SelectionType.DAYS) {
				model.setSelectionType(SelectionType.WEEK);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_DAYS, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_WEEK, null);
			} else if (type == SelectionType.WEEK) {
				model.setSelectionType(SelectionType.DATE);
				fireModelChangedEvent(CalendarChangesMask.UNSELECT_WEEK, null);
				fireModelChangedEvent(CalendarChangesMask.SELECT_FIRST_DAY,
						null);
				model.setCursorPosition(1);
			} else if (type == SelectionType.DATE) {
				model.setSelectionType(SelectionType.NONE);
			}
		}
		fireModelChangedEvent(CalendarChangesMask.SELECTION_TYPE, null);
	}

	/**
	 * Launches a value change event
	 * 
	 * @param d
	 *            The new date
	 */

	private void launchValueChangeEvent(Date d) {
		ValueChangeEvent.fire(component, d);
	}

	/**
	 * Launches a model changes event
	 * 
	 * @param action
	 * @param d
	 */

	public void fireModelChangedEvent(int action, Date d) {
		ModelChangedEvent.fire(this, action, d);
	}

	/**
	 * Launches a selection changes event
	 * 
	 */

	public void launchSelectionChangedEvent() {
		SelectionChangedEvent.fire(this, model.getSelectedDates());
	}

	/**
	 * Launches a invalid date event
	 * 
	 * @param d
	 */

	public void fireInvalidDateEvent(Date d) {
		InvalidDateEvent.fire(this, d, EventType.BUSY);
	}

	/**
	 * Reset the grid data
	 */

	private void resetGridData() {
		fireModelChangedEvent(CalendarChangesMask.CLEAR_GRID, null);
	}

	/**
	 * Display a date on the grid
	 * 
	 * @param g
	 *            The grid where display the date
	 * @param month
	 *            The month to display
	 * @param year
	 *            The year to display
	 */

	private void displayMonth(int month, int year) {
		model.setDisplayedMonth(month);
		model.setDisplayedYear(year);
		resetGridData();
		Date d = new Date();
		d.setMonth(month);
		d.setYear(year - 1900);
		fireModelChangedEvent(CalendarChangesMask.SPECIFIED_MONTH, d);
	}

	/**
	 * Manages TAB KEY press
	 */

	@Override
	public void onKeyDown(KeyDownEvent event) {
		switch (model.getSelectionCardinality()) {
		case MULTIPLE:
		case SINGLE:
			if (model.getSelectionType() != SelectionType.DATE) {
				event.preventDefault();
			}
			break;
		case NONE:
			if (model.getSelectionType() != SelectionType.MONTH) {
				event.preventDefault();
			}
			break;
		}
		int keyCode = event.getNativeKeyCode();
		if (keyCode == KeyCodes.KEY_TAB) {
			changeContextCursor();
		}
	}

	/**
	 * Add a model changed handler to the component
	 */

	@Override
	public HandlerRegistration addModelChangedHandler(
			ModelChangedHandler handler) {
		return addHandler(handler, ModelChangedEvent.getType());
	}

	/**
	 * Add a selection changed handler to the component
	 */

	@Override
	public HandlerRegistration addSelectionChangedHandler(
			SelectionChangedHandler handler) {
		return addHandler(handler, SelectionChangedEvent.getType());
	}

	/**
	 * Add a invalid date handler to the component
	 */

	@Override
	public HandlerRegistration addInvalidDateHandler(InvalidDateHandler handler) {
		return addHandler(handler, InvalidDateEvent.getType());
	}
}
